package com.slothought.rowgazer

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import javax.sql.DataSource;;

class TableMetadataFactory{
	
	static HashMap<QualifiedName, TableMetadata> jdbcTableMetadata;
	
	public static TableMetadata getTableMetadata(QualifiedName argTableQName, Connection connection){
		if (jdbcTableMetadata.containsKey(argTableQName)){
			return jdbcTableMetadata.get(argTableQName);
		}
		TableMetadata jTM;
		JDBCTableMetadataConverter converter = new JDBCTableMetadataConverter(argTableQName, connection);
		jTM = converter.getTableMetadata();
		return jTM;
	}
	
}

interface TableMetadataConverter{
	
	public TableMetadata getTableMetadata();
}

/**
 * @author leet
 *
 */
class JDBCTableMetadataConverter implements TableMetadataConverter{
    private boolean hasValidated = false
    private boolean isValid = false
    private boolean isProcessed = false
    
    //private Request request
    //private SchemaInfo schemaInfo
    private QualifiedName tableQname;
    private java.sql.Connection conn;
    
    private transient DatabaseMetaData md ;
    private transient ResultSet rset ;
    private transient def tmpIndexData;
    private transient def tmpFieldSets;
    private transient def tmpReferenceData ;
    
    private TableMetadata tableMetaData;
    
    public JDBCTableMetadataConverter(QualifiedName argTableQName, Connection conn){
        //this.request = argRequest
        this.tableQname = argTableQName
        this.conn = conn
        //this.schemaInfo = argSchemaData
    }
    
    
    public void validate(String userID, String password){
        if (hasValidated) return;
        
        try {
            this.isValid = false
            this.setup(userID, password);
            this.hasValidated = true;
            
        } catch (Exception e) {
            e.printStackTrace();
        } 
        finally{
            this.shutdown()
        }
        
    }
    
    public void process(String userID, String password){
        if (isProcessed) return;
        
        this.validate(userID, password);
        
        try {
            
            this.setup(userID, password);
            
            
        } catch (Exception e) {
            e.printStackTrace();
        } 
        finally{
            this.shutdown()
        }
    }
    
    private void setup(String userID, String password){
        
        //DataSource dataSource = DataSourceManager.getDataSource(this.schemaInfo.getDataSourceConfigName())
        //this.con = dataSource.getConnection();
        this.md = this.con.getMetaData() ;
        this.tmpReferenceData = [:];
    }
    
    private void shutdown(){
        this.md = null
        this.rset?.close();
        this.rset = null
        this.con?.close()
        this.con = null
        
    }
    
    
    
    /**
     * This updates all fk keys referenced or referencing the table
     * updates this.rset, and calls prepareKeysResultSet updates this.tmpTableData
     */
    private def prepareAllKeys(){
        
        this.rset =  this.md.getExportedKeys( this.tableQname.catalog, 
										      this.tableQname.schema, 
										      this.tableQname.name);
        
        this.prepareKeysResultSet(true);
        this.rset.close();
        
        this.rset =  this.md.getImportedKeys( this.tableQname.catalog, 
								              this.tableQname.schema, 
								              this.tableQname.name);
        
        this.prepareKeysResultSet(false);
        this.rset.close();
        
    }
    
    
    /**
     * This is called from prepareAllKeys
     * The logic is slightly different if it is exported key or imported key
     * @param isExportKeyResultSet 
     * reads this.rst and updates this.tmpTableData
     */
    private def prepareKeysResultSet( boolean isExportKeyResultSet){
    	
        while (this.rset.next()){
            
            String localColumnName = isExportKeyResultSet ? this.rset.getString("PKCOLUMN_NAME") : this.rset.getString("FKCOLUMN_NAME");
            String externalCatalogName = isExportKeyResultSet ? this.rset.getString("FKTABLE_CAT") : this.rset.getString("PKTABLE_CAT");
            String externalSchemaName = isExportKeyResultSet ? this.rset.getString("FKTABLE_SCHEM") : this.rset.getString("PKTABLE_SCHEM");
            String externalTableName = isExportKeyResultSet ? this.rset.getString("FKTABLE_NAME") : this.rset.getString("PKTABLE_NAME");
            String externalColumnName = isExportKeyResultSet ? this.rset.getString("FKCOLUMN_NAME") : this.rset.getString("PKCOLUMN_NAME");
            String keyName =  this.rset.getString("FK_NAME");
            
            
            if (this.tmpReferenceData[(keyName)] == null){
                TableMetadata.ReferentialConstraint tmpConstraint = new TableMetadata.ReferentialConstraint();
                tmpConstraint.tableTargetQName = new QualifiedName(externalCatalogName, externalSchemaName, externalTableName);
                tmpConstraint.fieldMapping[(localColumnName)] = (externalColumnName);
                tmpConstraint.isReferencing = isExportKeyResultSet ? true : false;
                //tmpConstraint.hasIndex = isExportKeyResultSet ? false : true;
                
                this.tmpReferenceData[(keyName)] = [    
                        columns : [localColumnName] as Set,
                        constraint : tmpConstraint,
                        keyName : keyName
                        ];
            }
            else{
                this.tmpReferenceData[(keyName)].columns.add(localColumnName);
                this.tmpReferenceData[(keyName)].constraints.fieldMapping[(localColumnName)] = (externalColumnName);
            }
        }
        
    }
    
    /**
     * Subprocedure to generate a list of fields
     * @return list of fields
     */
    private def prepareFields(){
    	
        this.rset =  this.md.getColumns(  this.tableQname.catalog, 
                this.tableQname.schema, 
                this.tableQname.name, null);
        
        def fieldList = [];
        while (this.rset.next()){
            
            
            String columnName =  this.rset.getString("COLUMN_NAME");
            def tmpFieldSet = [columnName] as Set;
            
            if (tmpIndexData.findAll{it.columns == tmpFieldSet}.size() == 0){
                fieldList.add(tmpFieldSet)
            }
        }
        
        this.rset.close();
        return fieldList;
        
    }
    
    /**
     * This prepares the indexes before the field set initialization
     * @return
     */
    private def prepareIndexes(){
    	def tmpIndexData = [:];
        tmpReferenceData.each {data,  entry ->
            if (entry.value.constraint != null){
                this.rset =  this.md.getIndexInfo(  this.tableQname.catalog, 
                        this.tableQname.schema, 
                        entry.value.constraint.tableTarget, false, false);
                
                
                
                while (this.rset.next()){
                    
                    String indexName =  this.rset.getString("INDEX_NAME");
                    String columnName =  this.rset.getString("COLUMN_NAME");
                    
                    if (tmpIndexData[(indexName)] == null){   
                        tmpIndexData[(indexName)] = [  columns : [columnName] as Set   ];
                    }
                    else{
                        tmpIndexData[(indexName)].columns.add(localColumnName);
                    }
                }
                this.rset.close();
                
                if (tmpIndexData.findAll{it.columns == entry.value.columns}.size() > 0){
                    entry.value.constraint.hasIndex = true
                }
            } 
        }
        return tmpIndexData
    }
    
    /**
     * 
     */
    private void prepareTableMetaData(){
        
        TableMetadata tableMD ;
        this.tmpReferenceData = [:];
        
        this.prepareAllKeys();
        this.prepareIndexes();
        this.prepareFields();
        
        
        tableMD = tmpReferenceData.inject(new TableMetadata()){ data,  entry ->
            
         	TableMetadata.FieldSet fs = new TableMetadata.FieldSet();
            
            fs.fields = (entry.value.columns);
            fs.references[(entry.value.keyName)]= entry.value.constraint;
            
            data.fieldsets.add(fs);
            return data;
        }
        
        tableMetaData = tmpFieldSets.inject (tableMD) { data,  entry ->
        	TableMetadata.FieldSet fs = new TableMetadata.FieldSet();
            fs.fields = (entry);
            data.fieldsets.add(fs);
            return data;
        }
        
        tableMetaData.table = tableName;
        
    }
    
    public TableMetadata getTableMetadata(){
    	prepareTableMetaData()
    	return tableMetadata
    }
    
    
}
